// SPDX-License-Identifier: MIT
// Copyright (C) 2018-present iced project and contributors

package com.github.icedland.iced.x86.info;

import com.github.icedland.iced.x86.Code;
import com.github.icedland.iced.x86.EncodingKind;
import com.github.icedland.iced.x86.MvexEHBit;
import com.github.icedland.iced.x86.internal.MvexInfo;
import com.github.icedland.iced.x86.internal.enc.LKind;

final class OpCodeFormatter {
	private final OpCodeInfo opCode;
	private final StringBuilder sb;
	private final int lkind;
	private final boolean hasModrmInfo;

	public OpCodeFormatter(OpCodeInfo opCode, StringBuilder sb, int lkind, boolean hasModrmInfo) {
		this.opCode = opCode;
		this.sb = sb;
		this.lkind = lkind;
		this.hasModrmInfo = hasModrmInfo;
	}

	public String format() {
		if (!opCode.isInstruction()) {
			switch (opCode.getCode()) {
			// GENERATOR-BEGIN: OpCodeFmtNotInstructionString
			// ⚠️This was generated by GENERATOR!🦹‍♂️
			case Code.INVALID:
				return "<invalid>";
			case Code.DECLAREBYTE:
				return "<db>";
			case Code.DECLAREWORD:
				return "<dw>";
			case Code.DECLAREDWORD:
				return "<dd>";
			case Code.DECLAREQWORD:
				return "<dq>";
			case Code.ZERO_BYTES:
				return "<zero_bytes>";
			// GENERATOR-END: OpCodeFmtNotInstructionString
			default:
				throw new UnsupportedOperationException();
			}
		}

		switch (opCode.getEncoding()) {
		case EncodingKind.LEGACY:
			return format_Legacy();
		case EncodingKind.VEX:
			return formatVecEncoding("VEX");
		case EncodingKind.EVEX:
			return formatVecEncoding("EVEX");
		case EncodingKind.XOP:
			return formatVecEncoding("XOP");
		case EncodingKind.D3NOW:
			return format_3DNow();
		case EncodingKind.MVEX:
			return formatVecEncoding("MVEX");
		default:
			throw new UnsupportedOperationException();
		}
	}

	private void appendHexByte(int value) {
		sb.append(String.format("%02X", value & 0xFF));
	}

	private void appendOpCode(int value, int valueLen, boolean sep) {
		if (valueLen == 1)
			appendHexByte((byte)value);
		else {
			assert valueLen == 2 : valueLen;
			appendHexByte((byte)(value >>> 8));
			if (sep)
				sb.append(' ');
			appendHexByte((byte)value);
		}
	}

	private void appendTable(boolean sep) {
		switch (opCode.getTable()) {
		case OpCodeTableKind.NORMAL:
			break;

		case OpCodeTableKind.T0F:
			appendOpCode(0x0F, 1, sep);
			break;

		case OpCodeTableKind.T0F38:
			appendOpCode(0x0F38, 2, sep);
			break;

		case OpCodeTableKind.T0F3A:
			appendOpCode(0x0F3A, 2, sep);
			break;

		case OpCodeTableKind.MAP5:
			sb.append("MAP5");
			break;

		case OpCodeTableKind.MAP6:
			sb.append("MAP6");
			break;

		case OpCodeTableKind.MAP8:
			sb.append("X8");
			break;

		case OpCodeTableKind.MAP9:
			sb.append("X9");
			break;

		case OpCodeTableKind.MAP10:
			sb.append("XA");
			break;

		default:
			throw new UnsupportedOperationException();
		}
	}

	private boolean hasModRM() {
		int opCount = opCode.getOpCount();
		if (opCount == 0)
			return false;

		switch (opCode.getEncoding()) {
		case EncodingKind.LEGACY:
		case EncodingKind.VEX:
			break;
		case EncodingKind.EVEX:
		case EncodingKind.XOP:
		case EncodingKind.D3NOW:
		case EncodingKind.MVEX:
			return true;
		default:
			throw new UnsupportedOperationException();
		}

		for (int i = 0; i < opCount; i++) {
			switch (opCode.getOpKind(i)) {
			// GENERATOR-BEGIN: HasModRM
			// ⚠️This was generated by GENERATOR!🦹‍♂️
			case OpCodeOperandKind.MEM:
			case OpCodeOperandKind.MEM_MPX:
			case OpCodeOperandKind.MEM_MIB:
			case OpCodeOperandKind.MEM_VSIB32X:
			case OpCodeOperandKind.MEM_VSIB64X:
			case OpCodeOperandKind.MEM_VSIB32Y:
			case OpCodeOperandKind.MEM_VSIB64Y:
			case OpCodeOperandKind.MEM_VSIB32Z:
			case OpCodeOperandKind.MEM_VSIB64Z:
			case OpCodeOperandKind.R8_OR_MEM:
			case OpCodeOperandKind.R16_OR_MEM:
			case OpCodeOperandKind.R32_OR_MEM:
			case OpCodeOperandKind.R32_OR_MEM_MPX:
			case OpCodeOperandKind.R64_OR_MEM:
			case OpCodeOperandKind.R64_OR_MEM_MPX:
			case OpCodeOperandKind.MM_OR_MEM:
			case OpCodeOperandKind.XMM_OR_MEM:
			case OpCodeOperandKind.YMM_OR_MEM:
			case OpCodeOperandKind.ZMM_OR_MEM:
			case OpCodeOperandKind.BND_OR_MEM_MPX:
			case OpCodeOperandKind.K_OR_MEM:
			case OpCodeOperandKind.R8_REG:
			case OpCodeOperandKind.R16_REG:
			case OpCodeOperandKind.R16_REG_MEM:
			case OpCodeOperandKind.R16_RM:
			case OpCodeOperandKind.R32_REG:
			case OpCodeOperandKind.R32_REG_MEM:
			case OpCodeOperandKind.R32_RM:
			case OpCodeOperandKind.R64_REG:
			case OpCodeOperandKind.R64_REG_MEM:
			case OpCodeOperandKind.R64_RM:
			case OpCodeOperandKind.SEG_REG:
			case OpCodeOperandKind.K_REG:
			case OpCodeOperandKind.KP1_REG:
			case OpCodeOperandKind.K_RM:
			case OpCodeOperandKind.MM_REG:
			case OpCodeOperandKind.MM_RM:
			case OpCodeOperandKind.XMM_REG:
			case OpCodeOperandKind.XMM_RM:
			case OpCodeOperandKind.YMM_REG:
			case OpCodeOperandKind.YMM_RM:
			case OpCodeOperandKind.ZMM_REG:
			case OpCodeOperandKind.ZMM_RM:
			case OpCodeOperandKind.CR_REG:
			case OpCodeOperandKind.DR_REG:
			case OpCodeOperandKind.TR_REG:
			case OpCodeOperandKind.BND_REG:
			case OpCodeOperandKind.SIBMEM:
			case OpCodeOperandKind.TMM_REG:
			case OpCodeOperandKind.TMM_RM:
				return true;
			// GENERATOR-END: HasModRM
			default:
				break;
			}
		}
		return false;
	}

	private boolean hasVsib() {
		int opCount = opCode.getOpCount();
		for (int i = 0; i < opCount; i++) {
			switch (opCode.getOpKind(i)) {
			// GENERATOR-BEGIN: HasVsib
			// ⚠️This was generated by GENERATOR!🦹‍♂️
			case OpCodeOperandKind.MEM_VSIB32X:
			case OpCodeOperandKind.MEM_VSIB64X:
			case OpCodeOperandKind.MEM_VSIB32Y:
			case OpCodeOperandKind.MEM_VSIB64Y:
			case OpCodeOperandKind.MEM_VSIB32Z:
			case OpCodeOperandKind.MEM_VSIB64Z:
				return true;
			// GENERATOR-END: HasVsib
			default:
				break;
			}
		}
		return false;
	}

	private int getOpCodeBitsOperand() {
		int opCount = opCode.getOpCount();
		for (int i = 0; i < opCount; i++) {
			int opKind = opCode.getOpKind(i);
			switch (opKind) {
			case OpCodeOperandKind.R8_OPCODE:
			case OpCodeOperandKind.R16_OPCODE:
			case OpCodeOperandKind.R32_OPCODE:
			case OpCodeOperandKind.R64_OPCODE:
				return opKind;
			}
		}
		return OpCodeOperandKind.NONE;
	}

	private static final class ModrmInfo {
		public final boolean isRegOnly;
		public final int rrr;
		public final int bbb;

		public ModrmInfo(boolean isRegOnly, int rrr, int bbb) {
			this.isRegOnly = isRegOnly;
			this.rrr = rrr;
			this.bbb = bbb;
		}
	}

	private ModrmInfo tryGetModrmInfo() {
		boolean isRegOnly = true;
		int rrr = opCode.getGroupIndex();
		int bbb = opCode.getRmGroupIndex();
		if (!hasModrmInfo)
			return null;

		int opCount = opCode.getOpCount();
		for (int i = 0; i < opCount; i++) {
			switch (opCode.getOpKind(i)) {
			case OpCodeOperandKind.MEM_OFFS:
			case OpCodeOperandKind.MEM:
			case OpCodeOperandKind.MEM_MPX:
			case OpCodeOperandKind.MEM_MIB:
				isRegOnly = false;
				break;
			case OpCodeOperandKind.MEM_VSIB32X:
			case OpCodeOperandKind.MEM_VSIB64X:
			case OpCodeOperandKind.MEM_VSIB32Y:
			case OpCodeOperandKind.MEM_VSIB64Y:
			case OpCodeOperandKind.MEM_VSIB32Z:
			case OpCodeOperandKind.MEM_VSIB64Z:
			case OpCodeOperandKind.SIBMEM:
				isRegOnly = false;
				bbb = 4;
				break;
			}
		}
		return new ModrmInfo(isRegOnly, rrr, bbb);
	}

	private void appendBits(String name, int bits, int numBits) {
		if (bits < 0)
			sb.append(name);
		else {
			for (int i = numBits - 1; i >= 0; i--) {
				if (((bits >>> i) & 1) != 0)
					sb.append('1');
				else
					sb.append('0');
			}
		}
	}

	private void appendRest() {
		ModrmInfo info = tryGetModrmInfo();
		if (info != null) {
			if (info.isRegOnly)
				sb.append(" 11:");
			else
				sb.append(" !(11):");
			appendBits("rrr", info.rrr, 3);
			sb.append(':');
			appendBits("bbb", info.bbb, 3);
		}
		else {
			boolean isVsib = (opCode.getEncoding() == EncodingKind.EVEX || opCode.getEncoding() == EncodingKind.MVEX) && hasVsib();
			if (opCode.isGroup()) {
				sb.append(" /");
				sb.append(opCode.getGroupIndex());
			}
			else if (!isVsib && hasModRM())
				sb.append(" /r");
			if (isVsib)
				sb.append(" /vsib");
		}

		int opCount = opCode.getOpCount();
		for (int i = 0; i < opCount; i++) {
			switch (opCode.getOpKind(i)) {
			case OpCodeOperandKind.BR16_1:
			case OpCodeOperandKind.BR32_1:
			case OpCodeOperandKind.BR64_1:
				sb.append(" cb");
				break;

			case OpCodeOperandKind.BR16_2:
			case OpCodeOperandKind.XBEGIN_2:
			case OpCodeOperandKind.BRDISP_2:
				sb.append(" cw");
				break;

			case OpCodeOperandKind.FARBR2_2:
			case OpCodeOperandKind.BR32_4:
			case OpCodeOperandKind.BR64_4:
			case OpCodeOperandKind.XBEGIN_4:
			case OpCodeOperandKind.BRDISP_4:
				sb.append(" cd");
				break;

			case OpCodeOperandKind.FARBR4_2:
				sb.append(" cp");
				break;

			case OpCodeOperandKind.IMM8:
			case OpCodeOperandKind.IMM8SEX16:
			case OpCodeOperandKind.IMM8SEX32:
			case OpCodeOperandKind.IMM8SEX64:
				sb.append(" ib");
				break;

			case OpCodeOperandKind.IMM16:
				sb.append(" iw");
				break;

			case OpCodeOperandKind.IMM32:
			case OpCodeOperandKind.IMM32SEX64:
				sb.append(" id");
				break;

			case OpCodeOperandKind.IMM64:
				sb.append(" io");
				break;

			case OpCodeOperandKind.XMM_IS4:
			case OpCodeOperandKind.YMM_IS4:
				sb.append(" /is4");
				break;

			case OpCodeOperandKind.XMM_IS5:
			case OpCodeOperandKind.YMM_IS5:
				sb.append(" /is5");
				// don't show the next imm8
				i = opCount;
				break;

			case OpCodeOperandKind.MEM_OFFS:
				sb.append(" mo");
				break;
			}
		}
	}

	private String format_Legacy() {
		sb.setLength(0);

		if (opCode.getFwait()) {
			appendHexByte(0x9B);
			sb.append(' ');
		}

		switch (opCode.getAddressSize()) {
		case 0:
			break;

		case 16:
			sb.append("a16 ");
			break;

		case 32:
			sb.append("a32 ");
			break;

		case 64:
			sb.append("a64 ");
			break;

		default:
			throw new UnsupportedOperationException();
		}

		switch (opCode.getOperandSize()) {
		case 0:
			break;

		case 16:
			sb.append("o16 ");
			break;

		case 32:
			sb.append("o32 ");
			break;

		case 64:
			// o64 (REX.W) must be immediately before the opcode and is handled below
			break;

		default:
			throw new UnsupportedOperationException();
		}

		switch (opCode.getMandatoryPrefix()) {
		case MandatoryPrefix.NONE:
			break;
		case MandatoryPrefix.PNP:
			sb.append("NP ");
			break;
		case MandatoryPrefix.P66:
			appendHexByte(0x66);
			sb.append(' ');
			break;
		case MandatoryPrefix.PF3:
			appendHexByte(0xF3);
			sb.append(' ');
			break;
		case MandatoryPrefix.PF2:
			appendHexByte(0xF2);
			sb.append(' ');
			break;
		default:
			throw new UnsupportedOperationException();
		}

		if (opCode.getOperandSize() == 64)
			sb.append("o64 ");

		appendTable(true);
		if (opCode.getTable() != OpCodeTableKind.NORMAL)
			sb.append(' ');
		appendOpCode(opCode.getOpCode(), opCode.getOpCodeLength(), true);
		switch (getOpCodeBitsOperand()) {
		case OpCodeOperandKind.R8_OPCODE:
			sb.append("+rb");
			break;
		case OpCodeOperandKind.R16_OPCODE:
			sb.append("+rw");
			break;
		case OpCodeOperandKind.R32_OPCODE:
			sb.append("+rd");
			break;
		case OpCodeOperandKind.R64_OPCODE:
			sb.append("+ro");
			break;
		case OpCodeOperandKind.NONE:
			break;
		default:
			throw new UnsupportedOperationException();
		}
		int opCount = opCode.getOpCount();
		for (int i = 0; i < opCount; i++) {
			if (opCode.getOpKind(i) == OpCodeOperandKind.STI_OPCODE) {
				sb.append("+i");
				break;
			}
		}

		appendRest();

		return sb.toString();
	}

	private String format_3DNow() {
		sb.setLength(0);

		appendOpCode(0x0F0F, 2, true);
		sb.append(" /r");
		sb.append(' ');
		appendOpCode(opCode.getOpCode(), opCode.getOpCodeLength(), true);

		return sb.toString();
	}

	private String formatVecEncoding(String encodingName) {
		sb.setLength(0);

		sb.append(encodingName);
		if (opCode.getEncoding() == EncodingKind.MVEX) {
			if (MvexInfo.isNDD(opCode.getCode()))
				sb.append(".NDD");
			else if (MvexInfo.isNDS(opCode.getCode()))
				sb.append(".NDS");
		}
		sb.append('.');
		if (opCode.isLIG())
			sb.append("LIG");
		else {
			switch (lkind) {
			case LKind.L128:
				sb.append(128 << opCode.getL());
				break;
			case LKind.L0:
				sb.append('L');
				sb.append(opCode.getL());
				break;
			case LKind.LZ:
				if (opCode.getL() != 0)
					throw new UnsupportedOperationException();
				sb.append("LZ");
				break;
			case LKind.NONE:
			default:
				throw new UnsupportedOperationException();
			}
		}
		switch (opCode.getMandatoryPrefix()) {
		case MandatoryPrefix.NONE:
		case MandatoryPrefix.PNP:
			break;
		case MandatoryPrefix.P66:
			sb.append('.');
			appendHexByte(0x66);
			break;
		case MandatoryPrefix.PF3:
			sb.append('.');
			appendHexByte(0xF3);
			break;
		case MandatoryPrefix.PF2:
			sb.append('.');
			appendHexByte(0xF2);
			break;
		default:
			throw new UnsupportedOperationException();
		}
		if (opCode.getTable() != OpCodeTableKind.NORMAL)
			sb.append('.');
		appendTable(false);
		if (opCode.isWIG())
			sb.append(".WIG");
		else {
			sb.append(".W");
			sb.append(opCode.getW());
		}
		if (opCode.getEncoding() == EncodingKind.MVEX) {
			switch (MvexInfo.getEHBit(opCode.getCode())) {
			case MvexEHBit.NONE:
				break;
			case MvexEHBit.EH0:
				sb.append(".EH0");
				break;
			case MvexEHBit.EH1:
				sb.append(".EH1");
				break;
			default:
				throw new UnsupportedOperationException();
			}
		}
		sb.append(' ');
		appendOpCode(opCode.getOpCode(), opCode.getOpCodeLength(), true);
		appendRest();

		return sb.toString();
	}
}
